package org.lex.game.sudoku.view;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.swing.event.EventListenerList;

import ms.sudoku.util.IntSet;

import org.lex.game.sudoku.SudokuApplication;
import org.lex.game.sudoku.datatype.BoardStatus;
import org.lex.game.sudoku.datatype.HintType;
import org.lex.game.sudoku.datatype.SelectType;
import org.lex.game.sudoku.model.Point;
import org.lex.game.sudoku.model.SudokuModel;
import org.lex.game.sudoku.model.SudokuModelListener;
import org.lex.game.sudoku.view.Operation.TimeComparator;
import org.lex.game.sudoku.view.cell.CellView;

import common.Sudokus;

public class SudokuBoardController extends AbstractController<SudokuBoard> implements SudokuModelListener, KeyListener,
		ActionListener, MouseListener {

	// SudokuBoard properties
	private SudokuModel sudokuModel = null;
	private BoardStatus status = BoardStatus.SOLVED;
	private EventListenerList listenerList = new EventListenerList();
	// current game start time
	private long startTime = 0L;
	// showing hints?
	private HintType hintType = HintType.NONE;
	// current selected cell
	private CellView selectedCell = null;

	public SudokuBoardController(SudokuApplication application, SudokuBoard view) {
		super(application, view);
	}

	Point getSelectedCell() {
		Point p = null;
		if (null != this.selectedCell) {
			p = new Point(this.selectedCell.getRow(), this.selectedCell.getColumn());
		}
		return p;
	}

	long getStartTime() {
		return this.startTime;
	}

	void setStartTime(long startTime) {
		this.startTime = startTime;
	}

	// Property: SudokuModel
	void setSudokuModel(SudokuModel model) {
		if (null != this.sudokuModel) {
			this.sudokuModel.removeListener(this);
		}
		this.sudokuModel = model;
		this.sudokuModel.addListener(this);
		this.updateBoard();
	}

	SudokuModel getSudokuModel() {
		return this.sudokuModel;
	}

	// Property: Status
	BoardStatus getStatus() {
		return this.status;
	}

	void setStatus(BoardStatus newStatus) {
		BoardStatus oldStatus = this.getStatus();
		if (oldStatus != newStatus) {
			this.status = newStatus;
			this.fireBoardCompleted(oldStatus, newStatus);
		}
	}

	boolean isShowHints() {
		return HintType.ALL == this.hintType;
	}

	// Property: showHints
	HintType getHintType() {
		return hintType;
	}

	void setHintType(HintType hintType) {
		boolean update = (this.hintType != hintType);
		this.hintType = hintType;
		if (true == update) {
			this.updateBoard();
		}
	}

	// SudokuBoardListener event support
	void addBoardListener(SudokuBoardListener listener) {
		this.listenerList.add(SudokuBoardListener.class, listener);
	}

	void removeBoardListener(SudokuBoardListener listener) {
		this.listenerList.remove(SudokuBoardListener.class, listener);
	}

	private void fireBoardCompleted(BoardStatus oldStatus, BoardStatus newStatus) {
		if (oldStatus != newStatus) {
			// Guaranteed to return a non-null array
			Object[] listeners = this.listenerList.getListenerList();
			// Process the listeners last to first, notifying
			// those that are interested in this event
			for (int i = listeners.length - 2; i >= 0; i -= 2) {
				if (listeners[i] == SudokuBoardListener.class) {
					((SudokuBoardListener) listeners[i + 1]).boardStatusChanged(this.getView(), oldStatus, newStatus);
				}
			}
		}
	}

	private void fireCellSelected(SelectType type, int row, int column) {
		// Guaranteed to return a non-null array
		Object[] listeners = this.listenerList.getListenerList();
		// Process the listeners last to first, notifying
		// those that are interested in this event
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == SudokuBoardListener.class) {
				((SudokuBoardListener) listeners[i + 1]).cellSelected(type, row, column);
			}
		}
	}

	void rollback(List<Operation> opList) {
		SudokuModel model = this.getView().getSudokuModel();
		Collections.sort(opList, Collections.reverseOrder(new TimeComparator()));
		for (Operation o : opList) {
			model.set(Cause.SYSTEM, o.getWhere().x, o.getWhere().y, o.getOldValue());
		}
		this.updateBoard();
	}

	// SudokuModelListener: listen for SudokuModel'cell changed
	public void cellChanged(SudokuModel model, Cause cause, int row, int col, int oldValue, int newValue) {
		log.debug("Model updated from [" + oldValue + "] to [" + newValue + "] at " + "(" + row + ", " + col + ")");
		// this.updateCell(row, col);

		// update ui
		this.updateBoard();
		// check complete
		if (true == this.getSudokuModel().isCompleted()) {
			if (this.getSudokuModel().checkAll(false).isValid()) {
				this.setStatus(BoardStatus.SOLVED);
			}
		}
	}

	// listen for SudokuModel changed
	public void modelChanged(SudokuModel model) {
		this.updateBoard();
	}

	// MouseListener: listen for cell been clicked

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
		CellView cell = (CellView) e.getSource();
		this.setSelectedCell(cell);
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	@Override
	public void mouseExited(MouseEvent e) {
		// TODO Auto-generated method stub

	}

	// ActionListener: listen for cell been clicked
	public void actionPerformed(ActionEvent e) {
		CellView cell = (CellView) e.getSource();
		this.setSelectedCell(cell);
	}

	private void setSelectedCell(CellView cell) {
		if (null != this.selectedCell) {
			CellView old = this.selectedCell;
			this.selectedCell = null;
			this.updateCell(old.getRow(), old.getColumn());
			this.fireCellSelected(SelectType.DESELECT, old.getRow(), old.getColumn());
		}
		this.selectedCell = cell;
		cell.requestFocus();
		this.updateCell(cell.getRow(), cell.getColumn());
		this.fireCellSelected(SelectType.SELECT, cell.getRow(), cell.getColumn());
	}

	// KeyListener: listen for user keyboard input
	@Override
	public void keyTyped(KeyEvent e) {
	}

	@Override
	public void keyPressed(KeyEvent e) {
	}

	public void keyReleased(KeyEvent e) {
		CellView cell = (CellView) e.getSource();
		char ch = e.getKeyChar();
		int keyCode = e.getKeyCode();
		log.debug("user input " + keyCode + "[" + KeyEvent.getKeyText(keyCode) + "] at (" + cell.getRow() + ", "
				+ cell.getColumn() + ")");
		if (KeyEvent.VK_BACK_SPACE == keyCode || KeyEvent.VK_DELETE == keyCode) {
			inputNumber(cell, Sudokus.EMPTY_NUMBER);
		} else if (KeyEvent.VK_UP == keyCode || KeyEvent.VK_LEFT == keyCode || KeyEvent.VK_DOWN == keyCode
				|| KeyEvent.VK_RIGHT == keyCode) {
			this.moveSelectedCell(cell, keyCode);
		} else if (ch >= '1' && ch <= '9') {
			inputNumber(cell, ch - '0');
		} else {
			log.warn("user input error: " + ch);
		}
	}

	private void inputNumber(CellView cell, int newValue) {
		int row = cell.getRow();
		int col = cell.getColumn();
		if (true == this.getSudokuModel().isEditable(row, col)) {
			this.getSudokuModel().set(Cause.USER, row, col, newValue);
		}
		// update ui
		// this.updateBoard();
	}

	private void moveSelectedCell(CellView cell, int keyCode) {
		if (null == this.selectedCell) {
			this.setSelectedCell(this.getView().getCellViews(0, 0));
		} else {
			int row = this.selectedCell.getRow();
			int col = this.selectedCell.getColumn();
			switch (keyCode) {
			case KeyEvent.VK_UP:
				row--;
				break;
			case KeyEvent.VK_DOWN:
				row++;
				break;
			case KeyEvent.VK_LEFT:
				col--;
				break;
			case KeyEvent.VK_RIGHT:
				col++;
				break;
			default:
				log.warn("user input error: keyCode=" + keyCode);
			}
			int gridSize = this.sudokuModel.getGridSize();
			row = (row + gridSize) % gridSize;
			col = (col + gridSize) % gridSize;
			this.setSelectedCell(this.getView().getCellViews(row, col));
		}
	}

	private void updateCell(int row, int col) {
		SudokuModel model = this.getSudokuModel();
		int num = model.get(row, col);
		CellView cell = this.getView().getCellViews(row, col);
		IntSet hints = this.getHints(row, col, num);

		boolean error = !model.checkCell(row, col, false).isValid();
		boolean editable = model.isEditable(row, col);
		boolean selected = this.isSelected(row, col);
		cell.set(model.get(row, col), hints, error, editable, selected);
	}

	void updateBoard() {
		SudokuModel model = this.getSudokuModel();
		SudokuBoard view = this.getView();
		// update all cells
		Set<Point> errorCells = model.checkAll(true).getDuplicates();
		for (int row = 0, gridSize = model.getGridSize(); row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				int num = model.get(row, col);
				Point currentLoc = new Point(row, col);
				boolean error = errorCells.contains(currentLoc);
				boolean editable = model.isEditable(row, col);
				boolean selected = this.isSelected(row, col);

				CellView cell = view.getCellViews(row, col);
				cell.set(num, this.getHints(row, col, num), error, editable, selected);
			}
		}
	}

	private IntSet getHints(int row, int col, int num) {
		return (this.isShowHints() && Sudokus.isEmpty(num)) ? this.getSudokuModel().getHints(row, col) : new IntSet();
	}

	private boolean isSelected(int row, int col) {
		return null == this.selectedCell ? false
				: (this.selectedCell.getRow() == row && this.selectedCell.getColumn() == col);
	}
}
